﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Globalization;
using System.Collections.Generic;
using System.Reflection;
using Xceed.Utils.Licensing;
using System.ComponentModel;

namespace Xceed.Silverlight.Controls
{
#if !WINDOWS_PHONE

  public class NumericTextBox : AutoSelectTextBox, INotifyPropertyChanged
  {
    #region STATIC MEMBERS

    private static NumberStyles DefaultNumberStyles = NumberStyles.Number;

    #endregion STATIC MEMBERS

    #region STATIC METHODS
    internal static bool IsNumericType( Type type )
    {
      if( type == null )
        return false;

      if( type.IsValueType )
      {
        if( ( type == typeof( int ) ) || ( type == typeof( double ) ) || ( type == typeof( decimal ) )
          || ( type == typeof( float ) ) || ( type == typeof( short ) ) || ( type == typeof( long ) )
          || ( type == typeof( ushort ) ) || ( type == typeof( uint ) ) || ( type == typeof( ulong ) )
          || ( type == typeof( byte ) ) || ( type == typeof( Nullable<int> ) ) || ( type == typeof( Nullable<double> ) )
          || ( type == typeof( Nullable<decimal> ) ) || ( type == typeof( Nullable<float> ) )
          || ( type == typeof( Nullable<short> ) ) || ( type == typeof( Nullable<long> ) )
          || ( type == typeof( Nullable<ushort> ) ) || ( type == typeof( Nullable<uint> ) )
          || ( type == typeof( Nullable<ulong> ) ) || ( type == typeof( Nullable<byte> ) )
          )
        {
          return true;
        }
      }

      return false;
    }

    #endregion

    #region CONSTRUCTORS

    public NumericTextBox()
    {
      this.AllowDrop = false;
      this.TextChanged += new TextChangedEventHandler( OnNumericTextChanged );
    }

    #endregion CONSTRUCTORS

    #region DifferTextChange PROPERTY
    private bool DifferTextChanged
    {
      get;
      set;
    }
    #endregion

    #region OnTextChange EventHandler

    void OnNumericTextChanged( object sender, TextChangedEventArgs e )
    {
      if( this.DifferTextChanged )
        return;

      int selectionStart = this.SelectionStart;

      try
      {

        this.DifferTextChanged = true;

        string compositionText = this.Text;

        if( String.IsNullOrEmpty( compositionText ) || String.IsNullOrWhiteSpace( compositionText ) )
        {
          if( this.Value != null )
            this.Value = null;
          return;
        }

        string resultingText = this.Text;

        if( String.IsNullOrEmpty( resultingText ) )
        {
          resultingText = string.Empty;
        }

        object notUsed;
        bool parsingSuccessful = this.TryParse( resultingText, out notUsed );

        if( !parsingSuccessful )
        {
          if( this.LastValidText != null )
          {
            this.Text = this.LastValidText;
          }
          else
          {
            this.Text = string.Empty;
          }
          this.SelectionStart = selectionStart;
        }
        this.LastValidText = this.Text;
        this.RefreshValue();
      }
      finally
      {
        this.DifferTextChanged = false;
      }

    }
    #endregion

    #region CustomDecimalKeys

    public List<Key> CustomDecimalKeys
    {
      get
      {
        if( m_customDecimalKeys == null )
          m_customDecimalKeys = new List<Key>( 1 );

        return m_customDecimalKeys;
      }
      set
      {
        if( value == null )
          throw new ArgumentNullException( "CustomDecimalKeys" );

        m_customDecimalKeys = value;
      }
    }

    #endregion CustomDecimalKeys

    #region NumberStyles PROPERTY

    public NumberStyles NumberStyles
    {
      get
      {
        return ( NumberStyles )GetValue( NumberStylesProperty );
      }
      set
      {
        SetValue( NumberStylesProperty, value );
      }
    }

    public static readonly DependencyProperty NumberStylesProperty = DependencyProperty.Register(
      "NumberStyles",
      typeof( NumberStyles ),
      typeof( NumericTextBox ),
      new PropertyMetadata( NumericTextBox.DefaultNumberStyles,
                            new PropertyChangedCallback( NumericTextBox.NumberStylesPropertyChanged ) ) );

    private static void NumberStylesPropertyChanged( DependencyObject sender, DependencyPropertyChangedEventArgs value )
    {
      NumericTextBox numericTextBox = sender as NumericTextBox;

      numericTextBox.ValidateNumberStyles( ( NumberStyles )value.NewValue );

    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly" )]
    private void ValidateNumberStyles( NumberStyles numberStyles )
    {
      if( Xceed.Utils.Licensing.DesignerProperties.GetIsInDesignMode() )
        return;

      string text = this.Text;

      object notUsed;
      bool parseSuccessful = this.TryParse( text, numberStyles, this.GetActiveFormatProvider(), out notUsed );

      if( !parseSuccessful )
        throw new InvalidOperationException( "The current text can not be parsed using the specified NumberStyles." );
    }

    #endregion NumberStyles PROPERTY

    #region LastValidText PROPERTY
    private string LastValidText
    {
      get;
      set;
    }
    #endregion

    #region FormatProvider Property

    public IFormatProvider FormatProvider
    {
      get
      {
        return ( IFormatProvider )GetValue( FormatProviderProperty );
      }
      set
      {
        SetValue( FormatProviderProperty, value );
      }
    }

    public static readonly DependencyProperty FormatProviderProperty =
        DependencyProperty.Register( "FormatProvider", typeof( IFormatProvider ), typeof( NumericTextBox ),
      new PropertyMetadata( null,
      new PropertyChangedCallback( NumericTextBox.FormatProviderPropertyChangedCallback ) ) );

    private static void FormatProviderPropertyChangedCallback( DependencyObject sender, DependencyPropertyChangedEventArgs e )
    {
      NumericTextBox numericTextBox = ( NumericTextBox )sender;

      if( !numericTextBox.IsControlInitialized )
        return;

      numericTextBox.OnFormatProviderChanged();
    }

    internal void OnFormatProviderChanged()
    {
      this.RefreshConversionHelpers();
      this.RefreshCurrentText( false );
      this.RefreshValue();
    }

    #endregion FormatProvider Property

    #region DataType Property

    public static readonly DependencyProperty DataTypeProperty = DependencyProperty.Register(
      "DataType",
      typeof( Type ),
      typeof( NumericTextBox ),
      new PropertyMetadata(
        ( Type )typeof( double ),
        new PropertyChangedCallback( NumericTextBox.OnDataTypeChanged ) ) );

    public Type DataType
    {
      get
      {
        return ( Type )this.GetValue( NumericTextBox.DataTypeProperty );
      }
      set
      {
        this.SetValue( NumericTextBox.DataTypeProperty, value );
      }
    }

    private static void OnDataTypeChanged( DependencyObject sender, DependencyPropertyChangedEventArgs e )
    {
      NumericTextBox textBox = sender as NumericTextBox;

      if( textBox == null )
        return;

      // Make sure initialization procedure is completed here
      if( !textBox.IsFinalizingInitialization )
      {
        textBox.IsNumericValueDataType = NumericTextBox.IsNumericType( textBox.DataType );
        textBox.RefreshConversionHelpers();
        textBox.RefreshValue();
      }

    }

    #endregion

    #region Value PROPERTY
    public object Value
    {
      get
      {
        return ( object )GetValue( ValueProperty );
      }
      set
      {
        SetValue( ValueProperty, value );
      }
    }

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register( "Value", typeof( object ), typeof( NumericTextBox ),
      new PropertyMetadata(
      new PropertyChangedCallback( NumericTextBox.ValuePropertyChangedCallback ) ) );


    private static void ValuePropertyChangedCallback( object sender, DependencyPropertyChangedEventArgs e )
    {
      NumericTextBox numericTextBox = sender as NumericTextBox;

      if( numericTextBox.IsForcingValue )
        return;

      // The ValueChangedCallback can be raised even though both values are the same since the property
      // datatype is Object.
      if( object.Equals( e.NewValue, e.OldValue ) )
        return;

      numericTextBox.IsInValueChanged = true;
      try
      {
        numericTextBox.Text = numericTextBox.GetTextFromValue( e.NewValue );
      }
      finally
      {
        numericTextBox.IsInValueChanged = false;
      }
    }

    #endregion Value Property

    #region HasValidationError Property

    public bool HasValidationError
    {
      get
      {
        return m_hasValidationError;
      }
      set
      {
        if( m_hasValidationError != value )
        {
          m_hasValidationError = value;
          this.RaisePropertyChanged( "HasValidationError" );
        }
      }
    }

    #endregion HasValidationError Property

    #region HasParsingError Property

    public bool HasParsingError
    {
      get
      {
        return m_hasParsingError;
      }
      set
      {
        if( m_hasParsingError != value )
        {
          m_hasParsingError = value;
          this.RaisePropertyChanged( "HasParsingError" );
        }
      }
    }

    #endregion HasParsingError Property

    #region TEXT FROM VALUE

    public event EventHandler<QueryTextFromValueEventArgs> QueryTextFromValue;

    internal string GetTextFromValue( object value )
    {
      string text = this.QueryTextFromValueCore( value );

      QueryTextFromValueEventArgs e = new QueryTextFromValueEventArgs( value, text );

      this.OnQueryTextFromValue( e );

      return e.Text;
    }

    private void OnQueryTextFromValue( QueryTextFromValueEventArgs e )
    {
      if( this.QueryTextFromValue != null )
        this.QueryTextFromValue( this, e );
    }

    #endregion TEXT FROM VALUE

    #region VALUE FROM TEXT

    public event EventHandler<QueryValueFromTextEventArgs> QueryValueFromText;

    internal object GetValueFromText( string text, out bool hasParsingError )
    {
      object value = null;
      bool success = this.QueryValueFromTextCore( text, out value );

      QueryValueFromTextEventArgs e = new QueryValueFromTextEventArgs( text, value );
      e.HasParsingError = !success;

      this.OnQueryValueFromText( e );

      hasParsingError = e.HasParsingError;

      return e.Value;
    }

    private void OnQueryValueFromText( QueryValueFromTextEventArgs e )
    {
      if( this.QueryValueFromText != null )
        this.QueryValueFromText( this, e );
    }

    #endregion VALUE FROM TEXT

    #region PROTECTED METHODS

    protected override void EndInitCore()
    {
      this.IsFinalizingInitialization = true;
      try
      {
        this.IsNumericValueDataType = NumericTextBox.IsNumericType( this.DataType );
        this.RefreshConversionHelpers();
      }
      catch( Exception exception )
      {
        throw new InvalidOperationException( "Initialization of the NumericTextBox failed.", exception );
      }
      finally
      {
        this.IsFinalizingInitialization = false;
      }
    }

    protected override void OnKeyDown( KeyEventArgs e )
    {
      base.OnKeyDown( e );

      if( !e.Handled )
      {
        if( e.Key == Key.Decimal )
        {
          string resultingText = this.Text;

          int oldSelectionStart = this.SelectionStart;

          if( String.IsNullOrEmpty( resultingText ) )
          {
            resultingText = string.Empty;
          }
          else
          {
            resultingText = resultingText.Remove( oldSelectionStart, this.SelectionLength );
          }

          CultureInfo currentCultureInfo = this.GetCultureInfo();

          resultingText = resultingText.Insert( this.SelectionStart, currentCultureInfo.NumberFormat.NumberDecimalSeparator );

          this.Text = resultingText;

          this.SelectionStart = oldSelectionStart + currentCultureInfo.NumberFormat.NumberDecimalSeparator.Length;

          e.Handled = true;
        }
      }
    }

    protected override void OnTextInput( System.Windows.Input.TextCompositionEventArgs e )
    {
      string compositionText = e.Text;

      if( String.IsNullOrEmpty( compositionText ) )
      {
        e.Handled = true;
        return;
      }

      string resultingText = this.Text;
      this.LastValidText = this.Text;

      if( String.IsNullOrEmpty( resultingText ) )
      {
        resultingText = string.Empty;
      }
      else
      {
        resultingText = resultingText.Remove( this.SelectionStart, this.SelectionLength );
      }

      resultingText = resultingText.Insert( this.SelectionStart, compositionText );

      object notUsed;
      bool parsingSuccessful = this.TryParse( resultingText, out notUsed );

      if( !parsingSuccessful )
      {
        e.Handled = true;
        return;
      }

      this.LastValidText = resultingText;
      base.OnTextInput( e );
    }

    protected override void OnGotFocus( RoutedEventArgs e )
    {
      base.OnGotFocus( e );

      this.RefreshCurrentText( true );
    }

    protected override void OnLostFocus( RoutedEventArgs e )
    {
      base.OnLostFocus( e );

      this.RefreshCurrentText( true );
    }

    protected bool QueryValueFromTextCore( string text, out object value )
    {
      Type type = this.DataType;

      if( type == null )
        throw new InvalidOperationException( "An attempt was made to query the value from the specified text when the DataType has not been specified." );

      return this.TryParse( text, out value );
    }

    protected string QueryTextFromValueCore( object value )
    {
      if( value != null )
      {
        if( ( this.NumberStyles & NumberStyles.HexNumber ) == NumberStyles.HexNumber )
        {
          return String.Format( this.GetActiveFormatProvider(), "{0:X}", value );
        }

        //This is required to make sure that default representation of value is not "against" the NumberStyles
        if( ( this.NumberStyles & NumberStyles.Number ) == NumberStyles.Number )
        {
          return String.Format( this.GetActiveFormatProvider(), "{0:N}", value );
        }
      }


      if( ( value == null ) || ( value == DBNull.Value ) )
        return string.Empty;

      IFormatProvider formatProvider = this.GetActiveFormatProvider();

      CultureInfo cultureInfo = formatProvider as CultureInfo;

      if( cultureInfo != null )
      {
        Type valueType = value.GetType();
        IConvertible convertibleInterface = valueType.GetInterface( "IConvertible", false ) as IConvertible;

        if( convertibleInterface != null )
          return ( string )convertibleInterface.ToString( this.FormatProvider );
      }

      try
      {
        string result = System.Convert.ToString( value, formatProvider );

        return result;
      }
      catch
      {
      }

      return value.ToString();

    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly" )]
    protected virtual void ValidateValue( object value )
    {
      if( value == null )
        return;

      Type validatingType = this.DataType;

      if( ( validatingType.IsGenericType ) && ( validatingType.GetGenericTypeDefinition() == typeof( Nullable<> ) ) )
        validatingType = Nullable.GetUnderlyingType( validatingType );

      if( validatingType == null )
        throw new InvalidOperationException( "An attempt was made to set a value when the DataType property is null." );

      if( ( value != DBNull.Value ) && ( value.GetType() != validatingType ) )
        throw new ArgumentException( "The value is not of type " + validatingType.Name + ".", "Value" );
    }

    #endregion PROTECTED METHODS

    #region INTERNAL PROPERTIES
    internal bool IsForcingValue
    {
      get;
      private set;
    }

    internal bool IsForcingText
    {
      get;
      private set;
    }

    internal bool IsNumericValueDataType
    {
      get;
      private set;
    }

    internal bool IsTextReadyToBeParsed
    {
      get
      {
        return true;
      }
    }

    #endregion

    #region INTERNAL METHODS

    internal void RefreshConversionHelpers()
    {
      Type type = this.DataType;

      if( type == null )
      {
        m_tryParseMethodInfo = null;
      }
      else
      {
        if( ( type.IsGenericType ) && ( type.GetGenericTypeDefinition() == typeof( Nullable<> ) ) )
          type = Nullable.GetUnderlyingType( type );

        m_tryParseMethodInfo = type.GetMethod(
          "TryParse",
          new Type[] { typeof( string ), typeof( NumberStyles ), typeof( IFormatProvider ), type.MakeByRefType() } );
      }

    }

    internal IFormatProvider GetActiveFormatProvider()
    {
      IFormatProvider formatProvider = this.FormatProvider;

      if( formatProvider != null )
        return formatProvider;

      return CultureInfo.CurrentCulture;
    }

    internal CultureInfo GetCultureInfo()
    {
      CultureInfo cultureInfo = this.GetActiveFormatProvider() as CultureInfo;

      if( cultureInfo != null )
        return cultureInfo;

      return CultureInfo.CurrentCulture;
    }

    internal string GetCurrentText()
    {
      return this.Text;
    }

    internal string GetParsableText()
    {
      return this.Text;
    }

    internal void RefreshCurrentText( bool preserveCurrentCaretPosition )
    {
      string displayText = this.GetCurrentText();

      if( !string.Equals( displayText, this.Text ) )
        this.ForceText( displayText, preserveCurrentCaretPosition );
    }

    internal void ForceValue( object value )
    {
      this.IsForcingValue = true;
      try
      {
        this.Value = value;
      }
      finally
      {
        this.IsForcingValue = false;
      }
    }

    internal void RefreshValue()
    {
      if( this.IsForcingValue )
        return;

      object value;
      bool hasParsingError;

      if( this.IsTextReadyToBeParsed )
      {
        string parsableText = this.GetParsableText();

        value = this.GetValueFromText( parsableText, out hasParsingError );

      }
      else
      {
        // We don't consider empty text as a parsing error.
        hasParsingError = !this.GetIsEditTextEmpty();
        value = null;
      }

      this.HasParsingError = hasParsingError;

      bool hasValidationError = hasParsingError;
      try
      {
        this.ValidateValue( value );
      }
      catch( Exception )
      {
        hasValidationError = true;
        value = null;
      }

      if( !object.Equals( value, this.Value ) )
        this.ForceValue( value );

      this.HasValidationError = hasValidationError;
    }

    internal bool GetIsEditTextEmpty()
    {
      return this.Text == string.Empty;
    }

    internal void ForceText( string text, bool preserveCaret )
    {
      this.IsForcingText = true;
      try
      {
        int oldSelectionStart = this.SelectionStart;

        this.Text = text;

        if( preserveCaret )
        {
          try
          {
            this.SelectionStart = oldSelectionStart;
          }
          catch( NullReferenceException )
          {
          }
        }
      }
      finally
      {
        this.IsForcingText = false;
      }
    }


    #endregion INTERNAL METHODS

    #region PRIVATE METHODS
    private bool TryParse( string text, out object parsedResult )
    {
      return this.TryParse( text, this.NumberStyles, this.GetActiveFormatProvider(), out parsedResult );
    }

    private bool TryParse( string text, NumberStyles numberStyles, IFormatProvider formatProvider, out object parsedResult )
    {
      parsedResult = null;

      if( text == string.Empty )
      {
        return true;
      }

      NumberFormatInfo numberFormatInfo = NumberFormatInfo.GetInstance( formatProvider );

      bool allowExponent = ( ( numberStyles & NumberStyles.AllowExponent ) == NumberStyles.AllowExponent );
      bool allowDecimalPoint = ( ( numberStyles & NumberStyles.AllowDecimalPoint ) == NumberStyles.AllowDecimalPoint );

      bool allowLeadingWhite = ( ( numberStyles & NumberStyles.AllowLeadingWhite ) == NumberStyles.AllowLeadingWhite );
      bool allowLeadingSign = ( ( numberStyles & NumberStyles.AllowLeadingSign ) == NumberStyles.AllowLeadingSign );

      string negativeSign = numberFormatInfo.NegativeSign;
      string positiveSign = numberFormatInfo.PositiveSign;

      string decimalSeparator = ( ( numberStyles & NumberStyles.Currency ) == NumberStyles.Currency ) ?
        numberFormatInfo.CurrencyDecimalSeparator : numberFormatInfo.NumberDecimalSeparator;

      if( ( allowExponent )
        && ( ( text.EndsWith( "e" ) ) || ( text.EndsWith( "E" ) )
        || ( text.EndsWith( "e" + negativeSign ) ) || ( text.EndsWith( "E" + negativeSign ) )
        || ( text.EndsWith( "e" + positiveSign ) ) || ( text.EndsWith( "E" + positiveSign ) ) ) )
      {
        text = text.Insert( text.Length, "0" );
      }

      if( allowLeadingWhite )
        text = text.TrimStart( new char[] { ' ' } );

      if( allowLeadingSign )
      {
        if( ( text.StartsWith( negativeSign ) && ( text.Length == negativeSign.Length ) )
          || ( text.StartsWith( positiveSign ) && ( text.Length == positiveSign.Length ) ) )
        {
          text = text.Insert( text.Length, "0" );
        }
      }

      if( ( allowDecimalPoint ) && ( text.EndsWith( decimalSeparator ) ) )
        text = text.Insert( text.Length, "0" );

      if( m_tryParseMethodInfo != null )
      {
        object[] tryParseParameters = new object[] { text, numberStyles, numberFormatInfo, null };

        if( ( bool )m_tryParseMethodInfo.Invoke( null, tryParseParameters ) )
        {
          parsedResult = ( object )tryParseParameters[ 3 ];
          return true;
        }
      }

      return false;
    }

    #endregion PRIVATE METHODS

    #region PRIVATE PROPERTIES
    internal bool IsInValueChanged
    {
      get;
      private set;
    }

    private bool IsFinalizingInitialization
    {
      get;
      set;
    }

    #endregion

    #region PRIVATE FIELDS

    private MethodInfo m_tryParseMethodInfo;
    private List<Key> m_customDecimalKeys;
    private bool m_hasValidationError;
    private bool m_hasParsingError;


    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged( string propertyName )
    {
      if( this.PropertyChanged != null )
      {
        this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
      }
    }


    #endregion

  }

#endif
}
